```
%% 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可.
%% 许可协议详情请见: http://creativecommons.org/licenses/by-nc-sa/4.0/
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}


\newcommand{\formatFigs}{./Figs}


\lhead{\bfseries SEED Labs -- 格式化字符串攻击实验}

\begin{document}

\begin{center}
{\LARGE 格式化字符串攻击实验}
\end{center}

\seedlabcopyright{2018 - 2020}

% *******************************************
% SECTION
% ******************************************* 
\section{概述}


C 语言中的 \texttt{printf()} 函数用于根据格式输出一个字符串。它的第一个参数被称为“格式化字符串”，它定义了如何格式化字符串。这些格式化字符串使用标记为 \texttt{\%} 的占位符，使得在打印过程中可以填充数据。格式化字符串不仅限于 \texttt{printf()} 函数；其他许多函数，如 \texttt{sprintf()}, \texttt{fprintf()}, 和 \texttt{scanf()} 也使用格式化字符串。如果某些程序允许用户提供整个或部分的格式化字符串内容，则这些内容未经过处理可能会导致攻击者通过利用这一机会使程序运行任意代码。这类问题被称为“格式化字符串漏洞”。

本次实验的目标是让学生将课堂上学到的关于这种漏洞的知识付诸实践，从而获得一手的经验。学生将会得到一个带有格式化字符串漏洞的程序；他们的任务是利用这个漏洞以实现以下破坏：(1) 程序崩溃，(2) 读取程序内部内存，(3) 修改程序内部内存，并且最为严重的是 (4) 利用受害者程序的权限注入并执行恶意代码。本实验覆盖了以下主题：

\begin{itemize}[noitemsep]
    \item 格式化字符串漏洞与代码注入
    \item 堆栈布局
    \item Shellcode 
    \item 逆向连接shell
\end{itemize}

\paragraph{参考资料和视频。}格式化字符串攻击的详细内容可以在以下资源中找到：

\begin{itemize}
    \item SEED 书籍第6章，\seedbook
    \item Udemy 上 SEED 课程中的第9节，\seedcsvideo
    \item 实验还涉及逆向连接shell，在SEED书中第9章有所介绍。
\end{itemize}

\paragraph{实验环境。} \seedenvironmentC

\paragraph{教师注意事项。}
教师可以根据需要调整此实验中变量 \texttt{L} 的值。详见 Section~\ref{sec:vulnerable_program} 中的详细说明。根据学生的背景知识和分配给本实验的时间，教师也可以选择对64位程序进行攻击是可选的，因为这更具挑战性。32位程序的攻击已经足够涵盖格式化字符串攻击的基础。

% *******************************************
% SECTION
% *******************************************
\section{环境设置} 

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{关闭防护措施}

现代操作系统使用地址空间随机化来随机化堆和栈的起始地址。这使得猜测准确的地址变得困难；猜出地址是格式化字符串攻击的关键步骤之一。为了简化实验任务，我们使用以下命令关闭了地址随机化：

\begin{lstlisting}
$ sudo sysctl -w kernel.randomize_va_space=0
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{漏洞程序}
\label{sec:vulnerable_program}

本实验中使用的漏洞程序称为 \texttt{format.c}，可以在 \texttt{server-code} 文件夹中找到。该程序包含一个格式化字符串漏洞，并且你的任务就是利用这个漏洞。下面列出的代码已移除了非必要的信息，因此与实验设置文件中的不同。

\begin{lstlisting}[label=format:code, 
       caption={带有非必要信息的漏洞程序 \texttt{format.c}}]
unsigned int  target = 0x11223344;
char *secret = "A secret message\n";

void myprintf(char *msg)
{
    // 这行代码包含格式化字符串漏洞
    printf(msg);
}

int main(int argc, char **argv)
{
    char buf[1500];
    int length = fread(buf, sizeof(char), 1500, stdin);
    printf("Input size: %d\n", length);

    myprintf(buf);

    return 1;
}
\end{lstlisting}

该程序从标准输入读取数据，然后将其传递给 \texttt{myprintf()} 函数，调用 \texttt{printf()} 输出数据。将输入数据喂入 \texttt{printf()} 函数的方式不安全，从而导致了格式化字符串漏洞。我们将利用这一漏洞。

此程序将在具有 root 权限的服务器上运行，并且其标准输入将被重定向到服务器与远程用户之间的 TCP 连接之间。因此，该程序实际上是从远程用户获取数据。如果用户能够利用这个漏洞，则可能会造成破坏。

\paragraph{编译。} 
我们将 \texttt{format} 程序分别编译为 32 位和 64 位二进制文件（对于 Apple Silicon 机器，我们仅编译 64 位二进制文件）。我们的预构建的 Ubuntu 20.04 虚拟机是一个 64 位虚拟机，但仍然支持 32 位二进制文件。我们只需在 \texttt{gcc} 命令中使用 \texttt{-m32} 选项即可。
对于 32 位编译，我们也使用 \texttt{-static} 来生成一个静态链接的二进制文件，该文件自包含且不依赖于任何动态库，因为我们的容器中未安装 32 位动态库。

编译命令已经提供在了 \texttt{Makefile} 中。要编译代码，请输入 \texttt{make} 执行这些命令。
完成编译后，我们需要将二进制文件复制到 \texttt{fmt-containers} 文件夹中，以便容器可以使用它们。以下命令执行编译和安装。

\begin{lstlisting}
$ make
$ make install
\end{lstlisting}

在编译过程中，你将会看到一条警告信息。此警告是由 \texttt{gcc} 编译器针对格式化字符串漏洞实现的一种防护措施生成的。我们可以暂时忽略这条警告。 

\begin{lstlisting}
format.c: 在函数 'myprintf' 中
format.c:33:5: 格式不为字符串常量且无格式参数 [-Wformat-security]
   33 |     printf(msg);
      |     ^~~~~~
\end{lstlisting}

需要注意的是，程序需要使用 \texttt{"-z execstack"} 编译选项进行编译，这允许堆栈是可执行的。我们的最终目标是在服务器程序的堆栈中注入代码并触发它。不可执行的堆栈是一种针对基于堆栈的代码注入攻击的防护措施，但可以通过另一种 SEED 实验中的 \texttt{return-to-libc} 技术绕过这一防护措施。在本实验中，为了简化起见，我们禁用了这个可以被绕过的防护措施。

\paragraph{教师注意事项。}
为了使本次实验与过去的版本有所不同，教师可以让学生通过编译服务器代码时使用不同的 \texttt{BUF\_SIZE} 值来改变这一变量的值。在 \texttt{Makefile} 中，\texttt{BUF\_SIZE} 的值由变量 \texttt{L} 设置。教师应根据 \texttt{format.c} 中的建议选择这个变量的具体值。

\paragraph{服务器程序。}
在 \texttt{server-code} 文件夹中，你可以找到一个名为 \texttt{server.c} 的程序。这是服务器的主要入口点。它侦听端口 \texttt{9090}。当收到 TCP 连接时，它会调用 \texttt{format} 程序，并将 TCP 连接设置为 \texttt{format} 程序的标准输入。因此，当 \texttt{format} 从 \texttt{stdin} 读取数据时，实际上是通过 TCP 连接获取的，即由 TCP 客户端上的用户提供数据。学生无需阅读 \texttt{server.c} 的源代码。

我们在服务器程序中添加了一点随机性，使得不同的学生可能会看到不同的内存地址和帧指针值。这些数值只有在容器重启时才会改变，因此只要你保持容器运行，就会看到相同的数字（不同学生的所见数字仍然不同）。这种随机性与地址空间随机化防护措施不同，其唯一目的是使学生的作业有所不同。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{容器设置和命令}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/container/setup}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% *******************************************
% SECTION
% *******************************************
\section{任务 1：程序崩溃}

当我们使用附带的 \texttt{docker-compose.yml} 文件启动容器时，将会启动两个容器，每个都运行一个存在漏洞的服务。本任务中，我们将使用位于 \texttt{10.9.0.5} 的服务器，它运行着一个具有格式化字符串漏洞的 32 位程序。对于 Apple Silicon 机器，这两个容器是相同的，并且它们都运行着 64 位服务程序（学生可以在本实验中选择任意一个）。

首先让我们向这个服务器发送一条无害的消息。你将在目标容器的日志输出中看到以下消息打印出来（实际显示的消息可能会有所不同）。

\begin{lstlisting}
$ echo hello | nc 10.9.0.5 9090
Press Ctrl+C

// 目标容器的控制台日志
server-10.9.0.5 | Got a connection from 10.9.0.1
server-10.9.0.5 | Starting format
server-10.9.0.5 | Input buffer (address):        0xffffd2d0
server-10.9.0.5 | The secret message's address:  0x080b4008
server-10.9.0.5 | The target variable's address: 0x080e5068
server-10.9.0.5 | Input size: 6
server-10.9.0.5 | Frame Pointer inside myprintf() = 0xffffd1f8
server-10.9.0.5 | The target variable's value (before): 0x11223344
server-10.9.0.5 | hello
server-10.9.0.5 | (^_^)(^_^) Returned properly (^_^)(^_^)
server-10.9.0.5 | The target variable's value (after):  0x11223344
\end{lstlisting}

服务器最多会接受你输入的 \texttt{1500} 字节的数据。你的主要任务是构造不同的负载以利用该服务中的格式化字符串漏洞，从而达到每个任务指定的目标。如果你将负载保存在一个文件中，可以使用以下命令向服务器发送负载。

\begin{lstlisting}
$ cat <file> | nc 10.9.0.5 9090
Press Ctrl+C  if it does not exit.
\end{lstlisting}

\paragraph{任务。}你的任务是提供一个输入给服务，使得当服务器程序在 \texttt{myprintf()} 函数尝试打印用户输入时会发生崩溃。你可以通过查看容器的日志输出来判断 \texttt{format} 程序是否已经崩溃。如果 \texttt{myprintf()} 返回，则会打印出 "Returned properly" 和一些笑脸；如果你看不到这些内容，那么 \texttt{format} 程序可能已经崩溃了。不过，服务器程序不会崩溃；崩溃的 \texttt{format} 程序在由服务器程序派生出来的子进程中运行。

由于本实验中构建的大多数格式化字符串都相当长，因此最好使用一个程序来完成这一任务。在 \texttt{attack-code} 目录中，我们为你准备了一个名为 \texttt{build\_string.py} 的样本代码，适用于对 Python 不熟悉的用户。它展示了如何将各种类型的数据放入一个字符串中。

% *******************************************
% SECTION
% *******************************************
\section{任务 2：打印服务器程序的内存数据}

本任务的目标是使服务器从其内存中打印出某些数据（我们仍会使用 \texttt{10.9.0.5}）。这些数据将在服务器端打印，因此攻击者无法看到。这并不是一个有意义的攻击，但是本任务所用的技术对于后续的任务非常重要。

\begin{itemize}
    \item \textbf{任务 2.A：堆栈数据。}
        目标是打印出你输入的数据在堆栈上的部分。你需要多少个 \texttt{\%x} 格式说明符，才能使服务器程序打印出你输入的前四个字节？你可以放入一些独特的数字（4 字节），以便它们被打印时你能立即分辨出来。这个数字对于大多数后续任务都很重要，请务必正确确定它。

    \item \textbf{任务 2.B：堆数据}
        在堆区域存储有一个秘密消息（字符串），你可以在服务器的日志中找到这个字符串的地址。你的任务是打印出这个秘密消息。
        要实现这一目标，你需要将秘密消息的地址（二进制形式）放入格式化字符串。

大多数计算机都是小端系统，因此在 32 位机器上要存储一个地址 \texttt{0xAABBCCDD}（四个字节），最小有效的字节 \texttt{0xDD} 应该存放在较低的地址中，而最有效的字节 \texttt{0xAA} 应该存放在较高的地址中。因此，在将地址存入缓冲区时，需要按照以下顺序保存：\texttt{0xDD}, \texttt{0xCC}, \texttt{0xBB}, 和 \texttt{0xAA}。在 Python 中，你可以这样做：

```python
number  = 0xAABBCCDD
content[0:4]  = (number).to_bytes(4,byteorder='little')
```
\end{itemize}

% *******************************************
% SECTION
% *******************************************
\section{任务 3：修改服务器程序的内存数据}

本任务的目标是更改 \texttt{target} 变量中的值，该变量定义在服务程序中（我们继续使用 \texttt{10.9.0.5})。原值为 \texttt{0x11223344}。假设这个变量存储了一个重要的值，可能会影响程序的控制流程。如果远程攻击者可以更改它的值，则可以改变该程序的行为。我们有三个子任务。

\begin{itemize}
    \item \textbf{任务 3.A：更改到不同的值。}
        在此子任务中，我们需要将 \texttt{target} 变量的内容更改为其他内容。如果你能成功将其更改为不同的值，则视为任务完成。从服务器的日志可以找到 \texttt{target} 变量的地址。

    \item \textbf{任务 3.B：更改到 \texttt{0x5000}.}
        在此子任务中，我们需要将 \texttt{target} 变量的内容更改为特定值 \texttt{0x5000}。只有当变量的值变为 \texttt{0x5000} 时，你的任务才算完成。

    \item \textbf{任务 3.C：更改到 \texttt{0xAABBCCDD}.}
        此子任务与前一个类似，唯一不同之处是目标值现在是一个较大的数字。在格式化字符串攻击中，这个值是通过 \texttt{printf()} 函数打印出的字符总数；打印出这么多字符可能需要几个小时。你需要使用一种更快的方法。基本思路是使用 \texttt{\%hn} 或 \texttt{\%hhn} 而不是 \texttt{\%n}，这样可以修改两个字节（或一个字节）的空间而不是四个字节。打印出 $2^{16}$ 个字符并不需要太多时间。更多细节请参阅 SEED 书籍。
\end{itemize}

% *******************************************
% SECTION
% *******************************************
\section{任务 4：在服务器程序中注入恶意代码}

现在我们准备好了对这一攻击的精华——代码注入进行最后的尝试。我们要将一段以二进制格式表示的恶意代码注入到服务器的内存中，然后利用格式化字符串漏洞修改函数返回地址字段，使得当该函数返回时跳转到我们的注入代码。

本任务所使用的技巧与前一个任务相似：它们都修改了内存中的 4 字节数字。在前一个任务中我们修改了 \texttt{target} 变量，在这个任务中则需要修改函数的返回地址字段。学生需要根据服务器提供的信息找到返回地址字段的地址（请在实验报告中包含你的答案）。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{理解堆栈布局}

\begin{figure}[htb]
\begin{center}
\includegraphics[width=0.8\textwidth]{\formatFigs/StackLayout.pdf}
\end{center}
\caption{当 \texttt{printf()} 从 \texttt{myprintf()} 函数内部被调用时的堆栈布局。}
\label{format:fig:stacklayout}
\end{figure}

为了成功完成这个任务，必须理解 \texttt{printf()} 被 \texttt{myprintf()} 内部调用时的堆栈布局。图~\ref{format:fig:stacklayout} 展示了这种布局。
需要注意的是我们故意在 \texttt{main} 和 \texttt{myprintf} 函数之间插入了一个空栈帧，但在图中没有显示出来。在完成此任务之前，学生需要回答以下问题（请将答案包含在实验报告中）：

\begin{itemize}[noitemsep]
    \item \textbf{问题 1：} 标记为 \ding{203} 和 \ding{204} 的位置的内存地址是什么？

    \item \textbf{问题 2：} 我们需要多少个 \texttt{\%x} 格式说明符才能将格式字符串参数指针移动到 \ding{204}？记住，参数指针从 \ding{202} 之上开始。
\end{itemize}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{Shellcode} 

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/guidelines/shellcode.tex}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

在 \texttt{attack-code} 目录中的 \texttt{exploit.py} 内包含了 32 位和 64 位版本的 Shellcode（对于 Apple Silicon 机器，仅包含 64 位 Shellcode）。你可以使用它们来构建格式化字符串。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{你的任务} 

请构造你的输入并传递给服务程序，证明你能够成功使服务器运行你的 Shellcode。在实验报告中，你需要解释你是如何构造这一格式化的字符串的，并且请在图~\ref{format:fig:stacklayout}中标出你的恶意代码的位置（请提供具体的地址）。

\paragraph{获取反向连接 shell。}
我们不关心运行一些预定义的命令，我们希望能够在目标服务器上获得 root shell 以便能够输入任何命令。因为我们是在远程机器上操作，所以如果只是让服务程序运行 \texttt{/bin/bash}，我们将无法控制 shell 程序。反向连接 shell 是解决这一问题的一种典型技术。Section~\ref{sec:guildelines} 提供了如何执行反向连接 shell 的详细说明。
请修改你 Shellcode 中的命令字符串，以便能够在目标服务器上获取反向连接 shell。请在实验报告中包含截图和解释。

% *******************************************
% SECTION
% *******************************************
\section{任务 5：攻击 64 位服务程序}

\textbf{注意：} 对于 Apple Silicon 机器，任务 1-4 已经使用了 64 位服务程序，因此此任务与任务 4 相同且不需要重复执行。然而，学生可以在本节中找到有关 64 位系统的有用信息。

在前面的任务中，我们的目标是 32 位程序。在这个任务中我们切换到一个 64 位的服务程序。我们的新目标是 \texttt{10.9.0.6}，它运行着 64 位版本的 \texttt{format} 程序。首先向这个服务器发送一条问候消息。
你将在目标容器的日志中看到以下信息打印出来。

\begin{lstlisting}
$ echo hello | nc 10.9.0.6 9090
Press Ctrl+C

// 目标容器控制台日志输出
server-10.9.0.6 | Got a connection from 10.9.0.1
server-10.9.0.6 | Starting format
server-10.9.0.6 | Input buffer (address):        0x00007fffffffe200
server-10.9.0.6 | The secret message's address:  0x0000555555556008
server-10.9.0.6 | The target variable's address: 0x0000555555558010
server-10.9.0.6 | Input size: 6
server-10.9.0.6 | Frame Pointer (inside myprintf):      0x00007fffffffe140
server-10.9.0.6 | The target variable's value (before): 0x1122334455667788
server-10.9.0.6 | hello
server-10.9.0.6 | (^_^)(^_^)  Returned from printf()  (^_^)(^_^)
server-10.9.0.6 | The target variable's value (after):  0x1122334455667788
\end{lstlisting}

你可以看到帧指针和缓冲区地址的值变为 8 字节（而在 32 位程序中为 4 字节）。你的任务是构造一个负载来利用服务程序中的格式化字符串漏洞。
你最终的目标是在目标服务器上获得 root shell。你需要使用 64 位版本的 Shellcode。

\paragraph{64 位地址带来的挑战。}由 x64 架构导致的一个挑战是地址中的零值。虽然 x64 架构支持 64 位地址空间，但只允许从 \texttt{0x00} 到 \texttt{0x00007FFFFFFFFFFF} 的地址。
这意味着每个地址（8 字节）的最高两个字节始终是零。这导致了一个问题。

在攻击过程中，我们需要将地址放入格式化字符串中。对于 32 位程序而言，可以随意放置这些地址，因为其中没有零值。现在我们不能再这样做。如果你在一个格式化字符串中间部分输入一个地址，在 \texttt{printf()} 解析格式化字符串时，它会在遇到第一个零后停止解析。基本上，格式化字符串中的任何在第一个零之后的内容都不会被视为格式化的。

由零引起的这一问题与缓冲区溢出攻击中出现的有所不同，在缓冲区溢出攻击中，如果使用 \texttt{strcpy}，则零值会终止内存复制操作。在这里程序没有进行内存复制，因此我们可以在输入中包含零，但如何放置这些零至关重要。
有许多方法可以解决这个问题，并将其留给学生。在实验报告中，学生们应该解释他们是如何解决这一问题的。

\paragraph{一种有用的技巧：自由移动参数指针。}在格式化字符串中，我们可以使用 \texttt{\%x} 来将可选参数指针 \texttt{va_list} 移动到下一个可选项参数上。
我们也可以直接将指针移到第 \texttt{k} 个可选项参数。这通过格式字符串的参数字段（形式为 \texttt{k\$}）完成。
下面的例子使用了 \texttt{"\%3\$.20x"} 打印出第 3 个可选项参数（编号为 3 的数字），并使用 \texttt{"\%6\$n"} 将一个值写入到第 6 个可选项参数中（变量 \texttt{var}，其值将变为 \texttt{20})。最后，
使用了 \texttt{\%2\$.10x} 将指针移回至第 2 个可选项参数（编号为 2），并将其打印出来。你可以看到，通过这种方法我们可以自由地来回移动指针。
这种技术可以简化此任务中格式化字符串的构建。

```c
#include <stdio.h>
int main()
{
    int var = 1000;
    printf("%3$.20x%6$n%2$.10x\n", 1, 2, 3, 4, 5, &var);
    printf("The value in var: %d\n",var);
    return 0;
}
----- 输出 ------
seed@ubuntu:$ a.out
000000000000000000030000000002
The value in var: 20
```

% *******************************************
% SECTION
% *******************************************
\section{任务 6：修复问题}

请解释由 \texttt{gcc} 编译器生成的警告信息是什么意思。请修复服务程序中的漏洞并重新编译它。编译器警告是否消失了？你的攻击是否仍然有效？你只需要尝试一个攻击来检查其是否仍能正常工作即可。

% *******************************************
% SECTION
% *******************************************
\section{逆向连接 Shell 的指南}
\label{sec:guildelines}

\input{\commonfolder/guidelines/reverse_shell.tex}

% *******************************************
% SECTION
% ******************************************* 
\section{提交作业}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/submission}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\end{document}
```